Python秘技:如何import不存在的对象
好久没更新了,大家久等了。平时写作时间确实较少,也并不想敷衍了事,也不想发没营养的文章,望各位海涵。《深异(中)》还有少许没有写完,所以下次再发。
今天是教大家一项 Python 秘技,如何 import 不存在的对象。源于小拳拳同学昨天在B乎上提了个问题:Python中模块变量__path__
在这段代码中怎么传进来的?他想弄明白下述问题里的魔法。
问题
先有这样一个模块bluesprint_factory.py
:
# bluesprint_factory.py
import inspect
import sys
from flask import Blueprint
class Wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
def __getattr__(self, key):
if key == 'bl':
AutoBlueprint = self.wrapped.AutoBlueprint
return AutoBlueprint()
return getattr(self.wrapped, key)
class AutoBlueprint(object):
def __init__(self):
frm = sys._getframe(2)
module = inspect.getmodule(frm)
pack_name = module.__name__.rsplit('.', 1)[-1]
self.bl = Blueprint(module.__name__, pack_name)
def __getattr__(self, key):
return getattr(self.bl, key)
sys.modules[__name__] = Wrapper(sys.modules[__name__])
然后在别的模块里可以这样写:
from bluesprint_factory import bl
如此,就能成功导入一个事先并不存在的bl
对象。而小拳拳自己在调试这段代码的时候发现Wrapper.__getattr__()
里面首先拦截到的key
值为'__path__'
,而不是意料中的'bl'
,为什么?更神奇的是,为何 return getattr(self.wrapped, key)
时,直接进入if key == 'bl':
里面去了?
解析
bluesprint_factory 模块的初始化
首先看下bluesprint_factory.py
做了什么事:
定义了一个
Wrapper
类;定义了一个
AutoBlueprint
类;把
bluesprint_factory
模块做了一次封装并替换。
在上述第3步中,进行了如下操作:
先从
sys.modules
中得到代表当前py模块被解析后的Module
对象,先称之为origin_module
;将
origin_module
对象传递给Wrapper
类,得到一个Wrapper
对象,称之为wrapped_module
,该对象只有一个值为origin_module
的属性self.wrapped
;再把
wrapped_module
对象绑定给sys.modules[__name__]
这个变量。
经上述过程后:
sys.modules[__name__] = wrapped_module
wrapped_module.wrapped = origin_module
到此为止,bluesprint_factory.py
的初始化工作就完成了。
import 语句背后的魔法
现在我们来看看从别的模块执行from bluesprint_factory import bl
会发生什么:
Python 解释器识别到这是一条
from x import y
格式的模块导入语句,会先判断x
是否为 package,而判断条件就是x
是否具有__path__
属性,不论__path__
是何值;import 机制从
sys.modules
中拿到bluesprint_factory
模块的Module
对象,即wrapped_module
,然后尝试访问它的__path__
属性;由于
wrapped_module
对象被实例化之后并无__path__
属性,于是调用了该对象的__getattr__()
方法,并传递参数'__path__'
,企图让__getattr__()
返回该属性的值(这里就是为什么__getattr__()
中先拦截到了'__path__'
);wrapped_module.__getattr__()
中第一次if
判断显然失败,随着执行return getattr(self.wrapped, key)
,等价于return getattr(orign_module, '__path__')
;显然
origin_module
也无该属性,返回None
,此时控制权回到了 import 机制中,而且知道了bluesprint_factory
就是一个正常的 module 而非 package,于是尝试从bluesprint_factory
模块中获取bl
对象;接着需要拿到代表
bluesprint_factory.py
的Module
对象,此时拿到的是wrapped_module
,如第2步,因为sys.modules
中相应的对象已被替换;相当于要取得
wrapped_module.bl
,跟第3步同理,bl
属性初始化时并不存在,于是进入wrapped_module.__getattr__()
,此时的key
值为'bl'
;于是
if key == 'bl'
条件成立,接着调用AutoBlueprint
创建实例并返回。
弄明白了 import 机制里的流程,我们知道不仅from bluesprint_factory import bl
可以触发上述流程,import bluesprint_factory; bl = bluesprint_factory.bl
也是可以的。
这整个导入流程都是由 Python 的 import 机制控制,相关源代码参考cpython/Python/import.c
、cpython/Lib/importlib
。
总结
现在我们已经学会了如何导入一个在初始化时并不存在的对象。如果我们清楚了解 Python 的 import 机制,借助 importlib
可以玩很多魔法,例如做热更新、猴子补丁、导入在运行时才能获取初始化参数的对象……
总的来说,import 机制就是 Python 解释器发现包、模块的眼睛,我们可以 hacking “眼睛” 的内部机制玩出逼真的障眼法以蒙蔽 Python 解释器。一般第三方库或框架的会采取这些魔法,让框架更为灵活强大,给使用框架的程序员带来便利,也降低框架在设计模式上的复杂度或代码量。
*END*
这里是 驹说码事,分享程序猿的码路历程
感谢您的关注